// Copyright 2021 Gravitational Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cli

import (
	"io/ioutil"
	"path/filepath"

	"github.com/gravitational/gravity/e/lib/environment"
	"github.com/gravitational/gravity/e/lib/install"
	"github.com/gravitational/gravity/e/lib/ops"
	"github.com/gravitational/gravity/e/lib/ops/resources/gravity"
	"github.com/gravitational/gravity/lib/defaults"
	"github.com/gravitational/gravity/lib/localenv"
	"github.com/gravitational/gravity/lib/ops/resources"
	"github.com/gravitational/gravity/lib/process"
	"github.com/gravitational/gravity/lib/processconfig"
	"github.com/gravitational/gravity/lib/state"
	"github.com/gravitational/gravity/lib/utils"
	"github.com/gravitational/gravity/tool/common"
	"github.com/gravitational/gravity/tool/gravity/cli"

	"github.com/gravitational/trace"
	kingpin "gopkg.in/alecthomas/kingpin.v2"
	"k8s.io/apimachinery/pkg/runtime"
)

// InstallConfig extends the open-source installer CLI config
type InstallConfig struct {
	// InstallConfig is the open-source installer CLI config
	cli.InstallConfig
	// License is the cluster License
	License string
	// LicenseFile is the path to the license file
	LicenseFile string
	// OpsAdvertiseAddr is the advertise address configuration for Ops Center
	OpsAdvertiseAddr string
	// RemoteOpsURL is the URL of the remote Ops Center
	RemoteOpsURL string
	// RemoteOpsToken is the auth token of the remote Ops Center
	RemoteOpsToken string
	// OperationID is the existing operation ID when installing via Ops Center
	OperationID string
	// OpsTunnelToken is the token used to connect to remote Ops Center
	OpsTunnelToken string
	// OpsSNIHost is the remote Ops Center SNI host
	OpsSNIHost string
}

// NewInstallConfig creates install config from the passed CLI args and flags
func NewInstallConfig(env *localenv.LocalEnvironment, g *Application) (*InstallConfig, error) {
	advertiseAddr := *g.InstallCmd.HubAdvertiseAddr
	if advertiseAddr == "" {
		advertiseAddr = *g.InstallCmd.OpsAdvertiseAddr
		if advertiseAddr != "" {
			common.PrintWarn("Flag --ops-advertise-addr is obsolete " +
				"and will be removed in future version, please " +
				"use --hub-advertise-addr instead.")
		}
	}
	config, err := cli.NewInstallConfig(env, g.Application)
	if err != nil {
		return nil, trace.Wrap(err)
	}
	return &InstallConfig{
		InstallConfig:    *config,
		License:          *g.InstallCmd.License,
		LicenseFile:      *g.InstallCmd.LicenseFile,
		OpsAdvertiseAddr: advertiseAddr,
		RemoteOpsURL:     *g.InstallCmd.OpsCenterURL,
		RemoteOpsToken:   *g.InstallCmd.OpsCenterToken,
		OperationID:      *g.InstallCmd.OperationID,
		OpsTunnelToken:   *g.InstallCmd.OpsCenterTunnelToken,
		OpsSNIHost:       *g.InstallCmd.OpsCenterSNIHost,
	}, nil
}

// newWizardConfig creates install config from the passed CLI args and flags
func newWizardConfig(env *environment.Local, g *Application) (*InstallConfig, error) {
	config, err := cli.NewWizardConfig(env.LocalEnvironment, g.Application)
	if err != nil {
		return nil, trace.Wrap(err)
	}
	return &InstallConfig{
		InstallConfig: *config,
	}, nil
}

// checkAndSetDefaults validates the configuration object and populates default values
func (i *InstallConfig) checkAndSetDefaults() (err error) {
	if i.OpsAdvertiseAddr != "" {
		if _, _, err := utils.ParseHostPort(i.OpsAdvertiseAddr); err != nil {
			return trace.Wrap(err, "failed to parse Gravity Hub advertise "+
				"address %q specified with --hub-advertise-addr flag, make "+
				"sure it's in the <hostname>:<port> format", i.OpsAdvertiseAddr)
		}
	}
	if i.RemoteOpsURL != "" && i.RemoteOpsToken == "" {
		return trace.BadParameter("missing RemoteOpsToken")
	}
	if err := i.InstallConfig.CheckAndSetDefaults(resources.ValidateFunc(gravity.Validate)); err != nil {
		return trace.Wrap(err)
	}
	return nil
}

// NewProcessConfig returns new gravity process configuration for this configuration object
func (i *InstallConfig) NewProcessConfig() (*processconfig.Config, error) {
	config, err := i.InstallConfig.NewProcessConfig()
	if err != nil {
		return nil, trace.Wrap(err)
	}
	if i.OpsAdvertiseAddr != "" {
		// in case of Ops Center install, its SNI host is the advertise hostname
		config.OpsCenter.SeedConfig.SNIHost, _ = utils.SplitHostPort(i.OpsAdvertiseAddr, "")
	} else {
		// in case of regular cluster install, the Ops Center SNI host might
		// have been provided on the CLI (e.g. by install instructions
		// generated by the Ops Center)
		config.OpsCenter.SeedConfig.SNIHost = i.OpsSNIHost
	}
	return config, nil
}

// LicenseFilePath returns path to the license file used for installation.
func (i *InstallConfig) LicenseFilePath() (string, error) {
	if i.LicenseFile != "" {
		// Resolve the path.
		path, err := filepath.Abs(i.LicenseFile)
		if err != nil {
			return "", trace.Wrap(err)
		}
		return path, nil
	}
	if i.License == "" {
		return "", trace.NotFound("no license provided")
	}
	// Use temporary install state directory to store the license content.
	path := state.GravityInstallDir("license")
	err := ioutil.WriteFile(path, []byte(i.License), defaults.PrivateFileMask)
	if err != nil {
		return "", trace.Wrap(err)
	}
	return path, nil
}

// NewInstallerConfig returns new installer configuration for this configuration object
func (i *InstallConfig) NewInstallerConfig(env *localenv.LocalEnvironment, wizard *localenv.RemoteEnvironment, process process.GravityProcess) (*install.Config, error) {
	ossConfig, err := i.InstallConfig.NewInstallerConfig(env, wizard, process)
	if err != nil {
		return nil, trace.Wrap(err)
	}
	var opsResources []runtime.Object
	if i.OpsAdvertiseAddr != "" {
		opsResources, err = ops.NewOpsCenterConfig(
			ops.OpsCenterConfigParams{
				AdvertiseAddr: i.OpsAdvertiseAddr,
				Devmode:       i.Insecure,
			})
		if err != nil {
			return nil, trace.Wrap(err)
		}
	}
	license := i.License
	if i.LicenseFile != "" {
		licenseBytes, err := ioutil.ReadFile(i.LicenseFile)
		if err != nil {
			return nil, trace.Wrap(err)
		}
		license = string(licenseBytes)
	}
	ossConfig.RuntimeResources = append(ossConfig.RuntimeResources, opsResources...)
	config := &install.Config{
		Config:           *ossConfig,
		OpsAdvertiseAddr: i.OpsAdvertiseAddr,
		License:          license,
		RemoteOpsURL:     i.RemoteOpsURL,
		RemoteOpsToken:   i.RemoteOpsToken,
		OperationID:      i.OperationID,
		OpsTunnelToken:   i.OpsTunnelToken,
		OpsSNIHost:       i.OpsSNIHost,
	}
	return config, nil
}

func parseArgs(args []string) (*kingpin.ParseContext, error) {
	app := kingpin.New("gravity", "")
	app.Terminate(func(int) {})
	app.Writer(ioutil.Discard)
	return RegisterCommands(app).ParseContext(args)
}
